package com.zsx.shopping.seckill.service.impl;

import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.zsx.shopping.common.api.coupon.feign.CouponFeignService;
import com.zsx.shopping.common.api.coupon.vo.SeckillSessionVO;
import com.zsx.shopping.common.api.coupon.vo.SeckillSkuVO;
import com.zsx.shopping.common.api.product.feign.ProductFeignService;
import com.zsx.shopping.common.api.product.vo.SaleAttrVO;
import com.zsx.shopping.common.api.product.vo.SkuVO;
import com.zsx.shopping.common.api.seckill.dto.SeckillOrderDto;
import com.zsx.shopping.common.exception.ShoppingException;
import com.zsx.shopping.common.session.UserVO;
import com.zsx.shopping.seckill.constant.SeckillConstant;
import com.zsx.shopping.seckill.converter.SeckillSkuConverter;
import com.zsx.shopping.common.api.seckill.enums.SeckillStatus;
import com.zsx.shopping.seckill.interceptor.LoginInterceptor;
import com.zsx.shopping.seckill.redis.RedisSeckillSku;
import com.zsx.shopping.seckill.service.SeckillService;
import com.zsx.shopping.seckill.vo.app.CurSeckillSkuVO;
import com.zsx.shopping.seckill.vo.app.SeckillGoodsVO;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Service
public class SeckillServiceImpl implements SeckillService {

    @Resource
    private CouponFeignService couponFeignService;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private ProductFeignService productFeignService;

    @Resource
    private RabbitTemplate rabbitTemplate;

    @Resource
    private SeckillSkuConverter seckillSkuConverter;

    @Override
    public void uploadSeckillSkuLatest3Days() {
        List<SeckillSessionVO> seckillSessionVOs = couponFeignService.getLatest3DaySession().getData();
        BoundHashOperations<String, String, RedisSeckillSku> hashOps = redisTemplate.boundHashOps(SeckillConstant.SECKILL_SKU);
        seckillSessionVOs.forEach(seckillSessionVO -> {
            long startTime = seckillSessionVO.getStartTime().toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
            long endTime = seckillSessionVO.getEndTime().toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
            String sessionKey = SeckillConstant.SECKILL_SESSIONS_PREFIX + startTime + "_" + endTime;

            // 判断Redis中是否有该场次信息，如果没有才进行添加
            Boolean hasKey = stringRedisTemplate.hasKey(sessionKey);
            if (hasKey == null || !hasKey) {
                List<SeckillSkuVO> seckillSkuVOs = seckillSessionVO.getSeckillSkus();

                List<Long> skuIds = seckillSkuVOs.stream().map(SeckillSkuVO::getSkuId).collect(Collectors.toList());
                // 调用商品服务缓存商品详情数据
                productFeignService.cacheGoods(skuIds);
                // 调用商品服务获取商品基本信息
                Map<Long, SkuVO> skuVOMap = productFeignService.getSkus(skuIds).getData()
                        .stream().collect(Collectors.toMap(SkuVO::getId, Function.identity()));

                // 保存该场次所有sessionId_skuId
                List<String> sessionId_skuId_list = seckillSkuVOs.stream().map(seckillSkuVO ->
                        seckillSessionVO.getId() + "_" + seckillSkuVO.getSkuId())
                        .collect(Collectors.toList());
                stringRedisTemplate.opsForList().leftPushAll(sessionKey, sessionId_skuId_list);

                seckillSkuVOs.forEach(seckillSkuVO -> {
                    String skuKey = seckillSessionVO.getId() + "_" + seckillSkuVO.getSkuId();
                    // 生成商品随机码
                    String token = UUID.randomUUID().toString().replace("-", "");
                    // 缓存秒杀商品
                    RedisSeckillSku redisSeckillSku = new RedisSeckillSku();
                    redisSeckillSku.setSkuId(seckillSkuVO.getSkuId());
                    redisSeckillSku.setSessionId(seckillSessionVO.getId());
                    redisSeckillSku.setSeckillPrice(seckillSkuVO.getSeckillPrice());
                    redisSeckillSku.setCount(seckillSkuVO.getSeckillCount());
                    redisSeckillSku.setLimit(seckillSkuVO.getSeckillLimit());
                    redisSeckillSku.setStartTime(startTime);
                    redisSeckillSku.setEndTime(endTime);
                    redisSeckillSku.setRandomCode(token);
                    SkuVO skuVO = skuVOMap.get(seckillSkuVO.getSkuId());
                    redisSeckillSku.setName(skuVO.getName());
                    redisSeckillSku.setPrice(skuVO.getPrice());
                    redisSeckillSku.setDefaultImage(skuVO.getDefaultImage());

                    List<SaleAttrVO> saleAttrVOs = skuVO.getSaleAttrs();
                    StringBuilder s = new StringBuilder();
                    if (!CollectionUtils.isEmpty(saleAttrVOs)) {
                        for (SaleAttrVO saleAttrVO : saleAttrVOs) {
                            s.append(saleAttrVO.getAttrName()).append("：").append(saleAttrVO.getAttrValue()).append(",");
                        }
                        s.deleteCharAt(s.length() - 1);
                    }
                    redisSeckillSku.setSaleAttrs(s.toString());
                    hashOps.put(skuKey, redisSeckillSku);

                    // 使用库存作为Redisson信号量(限流)
                    RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE_PREFIX + token);
                    semaphore.trySetPermits(seckillSkuVO.getSeckillCount());
                });
            }
        });
    }

    @Override
    public List<CurSeckillSkuVO> getCurSeckillSkus() {
        Set<String> keys = redisTemplate.keys(SeckillConstant.SECKILL_SESSIONS_PREFIX + "*");
        if (!CollectionUtils.isEmpty(keys)) {
            for (String key : keys) {
                String replace = key.replace(SeckillConstant.SECKILL_SESSIONS_PREFIX, "");
                String[] s = replace.split("_");
                long startTime = Long.parseLong(s[0]);
                long endTime = Long.parseLong(s[1]);
                long currentTime = LocalDateTime.now().toInstant(ZoneOffset.ofHours(8)).toEpochMilli();

                if (currentTime >= startTime && currentTime <= endTime) {
                    List<String> sessionId_skuId_list = stringRedisTemplate.opsForList().range(key, 0, -1);
                    if (!CollectionUtils.isEmpty(sessionId_skuId_list)) {
                        BoundHashOperations<String, String, RedisSeckillSku> hashOps = redisTemplate.boundHashOps(SeckillConstant.SECKILL_SKU);
                        List<RedisSeckillSku> redisSeckillSkus = hashOps.multiGet(sessionId_skuId_list);
                        return seckillSkuConverter.toCurSeckillSkuVOs(redisSeckillSkus);
                    }
                }
            }
        }
        return new ArrayList<>(0);
    }

    @Override
    public SeckillGoodsVO getSeckillGoods(Long skuId) {
        RedisSeckillSku redisSeckillSku = null;
        Integer curCount = null;
        BoundHashOperations<String, String, RedisSeckillSku> hashOps = redisTemplate.boundHashOps(SeckillConstant.SECKILL_SKU);
        Set<String> sessionId_skuId_set = hashOps.keys();
        if (!CollectionUtils.isEmpty(sessionId_skuId_set)) {
            String reg = "\\d_" + skuId;
            for (String sessionId_skuId : sessionId_skuId_set) {
                if (Pattern.matches(reg, sessionId_skuId)) {
                    RedisSeckillSku temp = hashOps.get(sessionId_skuId);
                    assert temp != null;
                    long startTime = temp.getStartTime();
                    long endTime = temp.getEndTime();
                    long currentTime = LocalDateTime.now().toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
                    curCount = (Integer) redisTemplate.opsForValue().get(SeckillConstant.SKU_STOCK_SEMAPHORE_PREFIX + temp.getRandomCode());
                    // 正在秒杀
                    if (currentTime >= startTime && currentTime <= endTime) {
                        return seckillSkuConverter.toSeckillGoodsVO(temp, curCount);
                    }
                    // 将要秒杀
                    if (currentTime < startTime) {
                        // 多个秒杀场次有同一商品,取更早的那个
                        if (redisSeckillSku == null || temp.getStartTime() < redisSeckillSku.getStartTime()) {
                            // 秒杀还未开始,不能暴露随机码
                            temp.setRandomCode("");
                            redisSeckillSku = temp;
                        }
                    }
                }
            }
        }
        return seckillSkuConverter.toSeckillGoodsVO(redisSeckillSku, curCount);
    }

    @Override
    public String kill(Long sessionId, Long skuId, Integer num, String code) {
        UserVO userVO = LoginInterceptor.loginUser.get();
        BoundHashOperations<String, String, RedisSeckillSku> hashOps = redisTemplate.boundHashOps(SeckillConstant.SECKILL_SKU);
        RedisSeckillSku redisSeckillSku = hashOps.get(sessionId + "_" + skuId);
        if (redisSeckillSku != null) {
            long startTime = redisSeckillSku.getStartTime();
            long endTime = redisSeckillSku.getEndTime();
            long currentTime = System.currentTimeMillis();

            // 检查秒杀请求是否在活动时间内
            if (currentTime >= startTime && currentTime <= endTime) {
                // 检查随机码和商品id
                String randomCode = redisSeckillSku.getRandomCode();
                if (randomCode.equals(code)) {
                    // 检查是否超过限购数量、库存量是否充足
                    // 这里库存量和后面分布式锁信号量相当于单例模式的双重检查,有效提高效率
                    Integer limit = redisSeckillSku.getLimit();
                    Integer count = (Integer) redisTemplate.opsForValue().get(SeckillConstant.SKU_STOCK_SEMAPHORE_PREFIX + randomCode);
                    assert count != null;
                    if (count > 0 && num <= limit && num <= count ) {
                        // 检查用户是否参与过该商品的秒杀(幂等性处理),若没参与过则去占位
                        String redisKey = userVO.getId() + "_" + sessionId + "_" + skuId;
                        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(SeckillConstant.SECKILL_STATUS_PREFIX + redisKey, SeckillStatus.PLACEHOLDER.getCode());
                        if (aBoolean != null && aBoolean) {
                            // 占位成功说明从来没有买过
                            RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE_PREFIX + randomCode);
                            if (semaphore.tryAcquire(num)) {
                                // 秒杀成功
                                redisTemplate.opsForValue().set(SeckillConstant.SECKILL_STATUS_PREFIX + redisKey, SeckillStatus.SUCCESS.getCode());
                                // 生成秒杀订单
                                SeckillOrderDto seckillOrderDto = new SeckillOrderDto();
                                String orderSn = IdWorker.getTimeId();
                                seckillOrderDto.setOrderSn(orderSn);
                                seckillOrderDto.setUserId(userVO.getId());
                                seckillOrderDto.setSkuId(redisSeckillSku.getSkuId());
                                seckillOrderDto.setSessionId(sessionId);
                                seckillOrderDto.setCount(num);
                                seckillOrderDto.setDefaultImage(redisSeckillSku.getDefaultImage());
                                seckillOrderDto.setSkuName(redisSeckillSku.getName());
                                seckillOrderDto.setSaleAttrs(redisSeckillSku.getSaleAttrs());
                                seckillOrderDto.setSeckillPrice(redisSeckillSku.getSeckillPrice());
                                seckillOrderDto.setRandomCode(randomCode);

                                // 发送秒杀订单到消息队列,订单服务监听并生成普通订单保存到数据库
                                rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order", seckillOrderDto);
                                // 订单服务生成订单后将本服务redis保存的秒杀状态置为"READY"
                                // 前端通过sessionId_skuId轮询秒杀状态,若为"READY"则跳转支付页进行支付
                                return sessionId + "_" + skuId + "_" + orderSn;
                            } else {
                                // 删除秒杀状态,使秒杀失败的用户可以再次参与同件商品的秒杀
                                redisTemplate.delete(SeckillConstant.SECKILL_STATUS_PREFIX + redisKey);
                                // 返回空字符串前端提示商品已售罄
                                throw new ShoppingException("商品已售罄");
                            }
                        } else {
                            Object o = redisTemplate.opsForValue().get(SeckillConstant.SECKILL_STATUS_PREFIX + redisKey);
                            if (o != null && SeckillStatus.READY.getCode().equals(Byte.valueOf(o.toString()))) {
                                throw new ShoppingException("您已参与过本次秒杀");
                            }
                        }
                    }
                }
            }
        }
        throw new ShoppingException();
    }

    @Override
    public void changeSeckillStatus(Long userId, Long sessionId, Long skuId, Byte status) {
        String redisKey = SeckillConstant.SECKILL_STATUS_PREFIX + userId + "_" + sessionId + "_" + skuId;
        redisTemplate.opsForValue().set(redisKey, status);
    }

    /**
     * 询问秒杀状态
     * 若秒杀状态为"失败",则进行删除(使用户可以再次参与秒杀),再返回前端
     * @param sessionId 场次编号
     * @param skuId 商品编号
     * @return 秒杀状态
     */
    @Override
    public Byte askSeckillStatus(Long sessionId, Long skuId) {
        UserVO userVO = LoginInterceptor.loginUser.get();
        String redisKey = SeckillConstant.SECKILL_STATUS_PREFIX + userVO.getId() + "_" + sessionId + "_" + skuId;
        Object o = redisTemplate.opsForValue().get(redisKey);
        if (o == null) {
            return null;
        }
        Byte status = Byte.valueOf(o.toString());
        if (SeckillStatus.FAIL1.getCode().equals(status)) {
            redisTemplate.delete(redisKey);
        }
        return status;
    }

    @Override
    public void restoreStock(String randomCode, Integer num) {
        redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE_PREFIX + randomCode).release(num);
    }
}
